package com.tn.openrpc.jms;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import javax.jms.ConnectionFactory;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.Session;
import javax.jms.TextMessage;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.jms.core.MessageCreator;
import org.springframework.util.ReflectionUtils;

import com.tn.openrpc.RemoteMethodException;
import com.tn.openrpc.channel.RemoteChannelException;
import com.tn.openrpc.channel.RemoteChannelRequest;
import com.tn.openrpc.channel.RemoteChannelResponse;
import com.tn.openrpc.serialization.SerializationException;
import com.tn.openrpc.serialization.Serializer;
import com.tn.openrpc.util.FactoryException;
import com.tn.openrpc.util.ParametrizedFactory;

/**
 * An implementation of <code>MessageProcessor</code> that processes a <code>RemoteChannelRequest</code> sending a
 * <code>RemoteChannelResponse</code> in response.
 */
public class RemoteChannelRequestProcessor implements MessageProcessor<TextMessage>
{
  private static final Logger LOGGER = LoggerFactory.getLogger(RemoteChannelRequestProcessor.class);

  private ConnectionFactory connectionFactory;
  private Serializer<String> serializer;
  private ParametrizedFactory<Object, String> targetFactory;

  /**
   * Creates a new <code>RemoteChannelRequestProcessor</code>.
   *
   * @param connectionFactory the connection factory used to connect to the destinations.
   * @param serializer        the serializer used to serialize requests and de-serialize the responses.
   * @param targetFactory     the factory that is used to create (or look-up) the target.
   */
  public RemoteChannelRequestProcessor(
    ConnectionFactory connectionFactory,
    Serializer<String> serializer,
    ParametrizedFactory<Object, String> targetFactory
  )
  {
    this.connectionFactory = connectionFactory;
    this.serializer = serializer;
    this.targetFactory = targetFactory;
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void processMessage(TextMessage message) throws MessageProcessingException
  {
    try
    {
      Object request = getRequestObject(message);

      if (request != null)
      {
        if (!(request instanceof RemoteChannelRequest))
        {
          LOGGER.error("Unhandled request type encountered: {}", request);
          return;
        }

        new JmsTemplate(connectionFactory).send(
          message.getJMSReplyTo(),
          new RemoteChannelResponseMessageCreator(
            invokeTargetMethod((RemoteChannelRequest)request),
            message.getJMSCorrelationID()
          )
        );
      }
    }
    catch (SerializationException e)
    {
      LOGGER.error("An error occurred de-serializing the message contents.", e);
    }
    catch (JMSException e)
    {
      LOGGER.error("An error occurred reading the message contents.", e);
    }
    catch (RuntimeException e)
    {
      LOGGER.error("An error occurred processing message.", e);
    }
  }

  /**
   * Returns the serializer.
   */
  public Serializer<String> getSerializer()
  {
    return serializer;
  }

  /**
   * Returns the request object from the <code>message</code>.
   */
  protected Object getRequestObject(TextMessage message) throws JMSException, SerializationException
  {
    return getSerializer().deserialize(message.getText());
  }

  /**
   * Invokes the target method according to the <code>request</code> and returns a response.
   */
  private RemoteChannelResponse invokeTargetMethod(RemoteChannelRequest remoteChannelRequest)
  {
    try
    {
      Object target = targetFactory.newInstance(remoteChannelRequest.getTarget());

      LOGGER.debug("Found target: {}", target);

      String methodName = remoteChannelRequest.getMethodName();
      List<Object> args = remoteChannelRequest.getArgs();

      Method targetMethod = findMethod(target, methodName, args);

      if (targetMethod == null)
      {
        LOGGER.error("Failed to find method for target: {} with name: {}", target, methodName);
        return new RemoteChannelResponse(
          new RemoteMethodException("Failed to find method for target: " + target + "  with name: " + methodName)
        );
      }

      LOGGER.debug("Invoking method: {} with args: {}", targetMethod, args);

      Object result = ReflectionUtils.invokeMethod(
        targetMethod,
        target,
        remoteChannelRequest.getArgs().toArray()
      );

      LOGGER.debug("Invoked method: {} and got result: {}", targetMethod, result);

      return new RemoteChannelResponse(result);
    }
    catch (FactoryException e)
    {
      LOGGER.error("An error occurred invoking the target method for request: " + remoteChannelRequest, e);
      return new RemoteChannelResponse(e);
    }
  }

  /**
   * Returns the <code>Method</code> with the <code>methodName</code> and <code>args</code> from the <code>target</code> if it exists;
   * otherwise <code>null</code>.
   */
  private Method findMethod(Object target, String methodName, List<Object> args)
  {
    List<Method> potentialMatches = new ArrayList<Method>();
    Class<?>[] argTypes = toClasses(args);

    //First check for exact match.
    methodLoop:
    for (Method method : target.getClass().getDeclaredMethods())
    {
      if (method.getName().equals(methodName) && method.getParameterTypes().length == args.size())
      {
        for (int i = 0; i < method.getParameterTypes().length; i++)
        {
          if (!method.getParameterTypes()[i].equals(argTypes[i]))
          {
            potentialMatches.add(method);
            continue methodLoop;
          }
        }

        return method;
      }
    }

    //If there was no exact match check the potential matches for assignable types.
    methodLoop:
    for (Method method : potentialMatches)
    {
      for (int i = 0; i < method.getParameterTypes().length; i++)
      {
        if (!method.getParameterTypes()[i].isAssignableFrom(argTypes[i]))
        {
          continue methodLoop;
        }
      }

      return method;
    }

    return null;
  }

  /**
   * Returns the classes for the <code>args</code>.
   */
  private Class<?>[] toClasses(List<Object> args)
  {
    List<Class<?>> classes = new ArrayList<Class<?>>();

    for (Object arg : args)
    {
      classes.add(arg != null ? arg.getClass() : null);
    }

    return classes.toArray(new Class<?>[classes.size()]);
  }

  /**
   * An implementation of <code>MessageCreator</code> that creates a <code>TextMessage</code> containing a
   * <code>RemoteChannelResponse</code>.
   */
  private class RemoteChannelResponseMessageCreator implements MessageCreator
  {
    private RemoteChannelResponse remoteChannelResponse;
    private String correlationId;

    /**
     * Creates a new <code>RemoteChannelResponseMessageCreator</code> initialized with the
     * <code>remoteChannelResponse</code> that will contained by the message this object creates.
     */
    public RemoteChannelResponseMessageCreator(RemoteChannelResponse remoteChannelResponse, String correlationId)
    {
      this.remoteChannelResponse = remoteChannelResponse;
      this.correlationId = correlationId;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Message createMessage(Session session) throws JMSException
    {
      try
      {
        Message message = session.createTextMessage(serializer.serialize(remoteChannelResponse));
        message.setJMSCorrelationID(correlationId);

        return message;
      }
      catch (SerializationException e)
      {
        throw new RuntimeException(new RemoteChannelException(e));
      }
    }
  }
}
